{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    pre-commit-hooks = {
      url = "github:cachix/pre-commit-hooks.nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    crane = {
      url = "github:ipetkov/crane";
    };
  };

  nixConfig = {
    extra-substituters = [ "https://tweag-nickel.cachix.org" ];
    extra-trusted-public-keys = [ "tweag-nickel.cachix.org-1:GIthuiK4LRgnW64ALYEoioVUQBWs0jexyoYVeLDBwRA=" ];
  };

  outputs =
    { self
    , nixpkgs
    , flake-utils
    , pre-commit-hooks
    , rust-overlay
    , crane
    }:
    let
      SYSTEMS = [
        "aarch64-darwin"
        "aarch64-linux"
        "x86_64-darwin"
        "x86_64-linux"
      ];

      cargoTOML = builtins.fromTOML (builtins.readFile ./Cargo.toml);
      cargoLock = builtins.fromTOML (builtins.readFile ./Cargo.lock);

      inherit (cargoTOML.workspace.package) version;

    in
    flake-utils.lib.eachSystem SYSTEMS (system:
    let
      pkgs = import nixpkgs {
        inherit system;
        overlays = [
          (import rust-overlay)
          # gnulib tests in diffutils fail for musl arm64, cf. https://github.com/NixOS/nixpkgs/pull/241281
          (final: prev: {
            diffutils =
              if !(final.stdenv.hostPlatform.isMusl && final.stdenv.hostPlatform.isAarch64) then
                prev.diffutils
              else
                prev.diffutils.overrideAttrs (old: {
                  postPatch = ''
                    sed -i 's:gnulib-tests::g' Makefile.in
                  '';
                });
          })
        ];
      };

      # Provide a `wasm-bindgen-cli` package which has the exact same version as
      # the Rust dependency `wasm-bindgen` locked in the Cargo.lock file. This
      # is needed to properly build the WASM REPL.
      wasm-bindgen-cli =
        let
          wasmBindgenCargoVersions = builtins.map ({ version, ... }: version) (builtins.filter ({ name, ... }: name == "wasm-bindgen") cargoLock.package);
          wasmBindgenVersion = assert builtins.length wasmBindgenCargoVersions == 1; builtins.elemAt wasmBindgenCargoVersions 0;
        in
        pkgs.buildWasmBindgenCli rec {
          src = pkgs.fetchCrate {
            pname = "wasm-bindgen-cli";
            version = wasmBindgenVersion;
            hash = "sha256-3RJzK7mkYFrs7C/WkhW9Rr4LdP5ofb2FdYGz1P7Uxog=";
          };

          cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
            inherit src;
            inherit (src) pname version;
            hash = "sha256-qsO12332HSjWCVKtf1cUePWWb9IdYUmT+8OPj/XP2WE=";
          };
        };

      # Additional packages required to build Nickel on Darwin
      systemSpecificPkgs =
        if pkgs.stdenv.isDarwin then
          [ pkgs.darwin.libiconv ]
        else
          [ ];

      mkRust =
        let
          inherit (pkgs.stdenv) hostPlatform;
        in
        { rustProfile ? "minimal"
        , rustExtensions ? [
            "rustfmt"
            "clippy"
          ]
        , targets ? [ pkgs.stdenv.hostPlatform.rust.rustcTarget ]
            ++ pkgs.lib.optional (!hostPlatform.isMacOS) pkgs.pkgsMusl.stdenv.hostPlatform.rust.rustcTarget
        }:
        (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
          extensions = rustExtensions;
          inherit targets;
        };

      # A note on check_format: the way we invoke rustfmt here works locally but fails on CI.
      # Since the formatting is checked on CI anyway - as part of the rustfmt check - we
      # disable rustfmt in the pre-commit hook when running checks, but enable it when
      # running in a dev shell.
      pre-commit-builder = { rust ? mkRust { }, checkFormat ? false }: pre-commit-hooks.lib.${system}.run {
        src = self;
        hooks = {
          nixpkgs-fmt = {
            enable = true;
            # Excluded because they are generated by Node2nix
            excludes = [
              "lsp/vscode-extension/default.nix"
              "lsp/vscode-extension/node-env.nix"
              "lsp/vscode-extension/node-packages.nix"
            ];
          };

          rustfmt = {
            enable = checkFormat;
            entry = pkgs.lib.mkForce "${rust}/bin/cargo-fmt fmt -- --check --color always";
          };

          markdownlint = {
            enable = true;
            excludes = [
              "notes/(.+)\\.md$"
              "^RELEASES\\.md$"
            ];
          };

          # We could use Topiary here, but the Topiary version pulled from Nix
          # and the one baked in Nickel could differ. It's saner that what we
          # check in the CI is matching exactly the formatting performed by the
          # `nickel` binary of this repo.
          nickel-format = {
            name = "nickel-format";
            description = "The nickel formatter";
            entry = "${pkgs.lib.getExe self.packages."${system}".default} format";
            types = [ "text" ];
            enable = true;
            # Some tests are currently failing the idempotency check, and
            # formatting is less important there. We at least want the examples
            # as well as the stdlib to be properly formatted.
            files = "\\.ncl$";
            excludes = [
              "/tests/(.+)\\.ncl$"
            ];
          };
        };
      };

      # Customize source filtering for Crane as Nickel uses non-standard-Rust
      # files like `*.lalrpop`.
      filterNickelSrc = filterCargoSources:
        let
          mkFilter = regexp: path: _type: builtins.match regexp path != null;
          lalrpopFilter = mkFilter ".*lalrpop$";
          nclFilter = mkFilter ".*ncl$";
          txtFilter = mkFilter ".*txt$";
          snapFilter = mkFilter ".*snap$";
          scmFilter = mkFilter ".*scm$";
          # include markdown files for checking snippets in the documentation
          mdFilter = mkFilter ".*md$";
          cxxFilter = mkFilter ".*(cc|hh)$";
          # include all files that are used in tests
          importsFilter = mkFilter ".*/core/tests/integration/inputs/imports/imported/.*$";
          testsInputsFilter = mkFilter ".*/cli/tests/integration/inputs/.*$";
          cliTestsImportsFilter = mkFilter ".*/cli/tests/snapshot/imports/.*$";
          nlsTestsInputsFilter = mkFilter ".*/lsp/nls/tests/.*$";
        in
        pkgs.lib.cleanSourceWith {
          src = pkgs.lib.cleanSource ./.;

          # Combine our custom filters with the default one from Crane
          # See https://github.com/ipetkov/crane/blob/master/docs/API.md#libfiltercargosources
          filter = path: type:
            builtins.any (filter: filter path type) [
              lalrpopFilter
              nclFilter
              txtFilter
              snapFilter
              scmFilter
              mdFilter
              cxxFilter
              filterCargoSources
              importsFilter
              testsInputsFilter
              cliTestsImportsFilter
              nlsTestsInputsFilter
            ];
        };

      # if we directly set the revision, it would invalidate the cache on every commit.
      # instead we set a static dummy hash and edit the binary in a separate (fast) derivation.
      dummyRev = "DUMMYREV_THIS_SHOULD_NOT_APPEAR_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

      # pad a string with the tail of another string
      padWith = pad: str:
        str +
        builtins.substring
          (builtins.stringLength str)
          (builtins.stringLength pad)
          pad;

      # We want `nickel --version` and `nls --version` to print the git revision
      # that nickel was compiled from. However, putting self.shortRev in a
      # derivation invalidates the cache on any change, even if otherwise the
      # derivation is identical. To mitigate this, we pass an unchanging string
      # as the revision in `NICKEL_NIX_BUILD_REV`, and then have a small wrapper
      # that replaces that string in the output binary. On every new commit this
      # fast derivation will have to be rebuilt, but the slow compilation of
      # rust code will only happen on more substantial changes. This is only
      # needed for binaries that actually make use of this information (the
      # cli and the language server).
      fixupGitRevision = pkg: pkgs.stdenv.mkDerivation {
        pname = pkg.pname + "-rev-fixup";
        inherit (pkg) version meta;
        src = pkg;
        buildInputs = [ pkgs.bbe ]
          ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ pkgs.darwin.autoSignDarwinBinariesHook ];
        phases = [ "fixupPhase" ];
        fixupPhase = ''
          runHook preFixup

          mkdir -p $out/bin
          for srcBin in $src/bin/*; do
            outBin="$out/bin/$(basename $srcBin)"
        '' +
        # [dirty] must have 7 characters to match dummyRev (hard coded in
        # nickel-lang-cli and nickel-lang-lsp)
        # we have to pad them out to the same length as dummyRev so they fit
        # in the same spot in the binary
        ''
            bbe -e 's/${dummyRev}/${padWith dummyRev (self.shortRev or "[dirty]")}/' \
              $srcBin > $outBin
            chmod +x $outBin
          done

          runHook postFixup
        '';
      };

      craneOverrideToolchain =
        (crane.mkLib pkgs).overrideToolchain;

      # Given a rust toolchain, provide Nickel's Rust dependencies, Nickel, as
      # well as rust tools (like clippy)
      mkCraneArtifacts = { rust ? mkRust { }, noRunBench ? false, profile ? "release" }:
        let
          craneLib = craneOverrideToolchain rust;

          # suffixes get added via pnameSuffix
          pname = "nickel-lang";

          # Customize source filtering as Nickel uses non-standard-Rust files like `*.lalrpop`.
          src = filterNickelSrc craneLib.filterCargoSources;

          # set of cargo args common to all builds
          cargoBuildExtraArgs = "--frozen --offline";

          # Build *just* the cargo dependencies, so we can reuse all of that work (e.g. via cachix) when running in CI
          mkCargoArtifactsDeps = { cargoExtraArgs ? "", prevArtifacts ? false }: craneLib.buildDepsOnly ({
            inherit pname src;
            cargoExtraArgs = "${cargoBuildExtraArgs} ${cargoExtraArgs} --workspace";

            # pyo3 needs a Python interpreter in the build environment
            # https://pyo3.rs/v0.17.3/building_and_distribution#configuring-the-python-version
            nativeBuildInputs = with pkgs; [ pkg-config python3 ];
            buildInputs = [
              pkgs.nix
              pkgs.boost # implicit dependency of nix
            ];

            # seems to be needed for consumer cargoArtifacts to be able to use
            # zstd mode properly
            installCargoArtifactsMode = "use-zstd";
            CARGO_PROFILE = profile;
          } // { cargoArtifacts = prevArtifacts; });

          # Some of our builds use --all-features and others don't. We build the deps with both options
          # and gather all the artifacts together.
          cargoArtifactsAllFeaturesDeps = mkCargoArtifactsDeps { cargoExtraArgs = "--all-features"; };
          cargoArtifactsDeps = mkCargoArtifactsDeps { prevArtifacts = cargoArtifactsAllFeaturesDeps; };

          env = {
            NICKEL_NIX_BUILD_REV = dummyRev;
          };

          buildWorkspace = { pnameSuffix ? "", extraNickelFeatures ? [ ], extraBuildArgs ? "", extraArgs ? { } }:
            let
              featuresArg = pkgs.lib.optionalString
                (extraNickelFeatures != [ ])
                ("--features " + pkgs.lib.strings.concatStringsSep " " extraNickelFeatures);
            in
            # The `cargoArtifacts` are the dependencies of the workspace, so we
              # can reuse them in the build.
            craneLib.buildPackage ({
              inherit
                pname
                pnameSuffix
                src
                version
                cargoArtifacts;

              # pyo3 needs a Python interpreter in the build environment
              # https://pyo3.rs/v0.17.3/building_and_distribution#configuring-the-python-version
              nativeBuildInputs = with pkgs; [ pkg-config python3 ];
              # A git binary is needed for some of the tests
              buildInputs = with pkgs;
                [ git ]
                  ++ pkgs.lib.optionals
                  (builtins.elem "nix-experimental" extraNickelFeatures)
                  ([ boost nix ]);

              cargoExtraArgs = "${cargoBuildExtraArgs} ${featuresArg} ${extraBuildArgs} --workspace";
              CARGO_PROFILE = profile;
            } // extraArgs);

          # To build Nickel and its dependencies statically we use the musl
          # libc and clang with libc++ to build C and C++ dependencies. We
          # tried building with libstdc++ but without success.
          buildStaticWorkspace = { pnameSuffix, extraNickelFeatures ? [ ], extraBuildArgs ? "", extraArgs ? { } }:
            let
              # The `cc` crate (which is used by tree-sitter) gets its C
              # compilers from env variables like CC_x86_64_unknown_linux_musl.
              # (It's important that we don't override the more general CC env
              # variable because we want things like build scripts to use the
              # default platform compiler.)
              env_tgt = builtins.replaceStrings [ "-" ] [ "_" ] pkgs.pkgsMusl.stdenv.hostPlatform.rust.rustcTarget;
            in
            (buildWorkspace {
              inherit pnameSuffix extraNickelFeatures extraBuildArgs;
              extraArgs = {
                inherit env;
                CARGO_BUILD_TARGET = pkgs.pkgsMusl.stdenv.hostPlatform.rust.rustcTarget;
                doCheck = false;
                CARGO_PROFILE = profile;
                "CC_${env_tgt}" = "${pkgs.pkgsMusl.gcc}/bin/gcc";
                # We used to support building the nix-experimental feature
                # statically but we don't anymore because some of the pkgsMusl
                # stuff isn't in the binary cache, and building them was taking
                # hours in CI. Here are the incantations that allowed statically
                # linking nix:
                # RUSTFLAGS = "-L${pkgs.pkgsMusl.llvmPackages.libcxx}/lib -lstatic=c++abi -C link-arg=-lc";
                # CXXSTDLIB = "static=c++";
                # stdenv = p: p.pkgsMusl.libcxxStdenv;
              } // extraArgs;
            });

          buildCApi = { pnameSuffix ? "", extraBuildArgs ? "", extraArgs ? { } }:
            craneLib.buildPackage ({
              inherit
                pname
                pnameSuffix
                src
                version
                cargoArtifacts;

              cargoExtraArgs = "${cargoBuildExtraArgs} ${extraBuildArgs} --features capi --package nickel-lang";
              CARGO_PROFILE = profile;

              postInstall = ''
                mkdir -p $out/include
                ${pkgs.rust-cbindgen}/bin/cbindgen nickel --lockfile ./Cargo.lock --output $out/include/nickel_lang.h --cpp-compat
              '';
            } // extraArgs);

          shrinkStaticLib = { pnameSuffix ? "", extraBuildArgs ? "", extraArgs ? { } }:
            let
              cApi = buildCApi { inherit pnameSuffix extraBuildArgs extraArgs; };
            in
            pkgs.stdenv.mkDerivation {
              name = pname + pnameSuffix;
              src = ./scripts/shrink_archive.sh;
              dontUnpack = true;
              nativeBuildInputs = [ pkgs.llvmPackages.bintools ];
              buildPhase = ''
                ${pkgs.bash}/bin/bash $src -o libnickel_lang.a ${cApi}/lib/libnickel_lang.a
              '';
              installPhase = ''
                mkdir -p $out/lib
                mkdir -p $out/include

                cp libnickel_lang.a $out/lib/libnickel_lang.a
                if [ -e ${cApi}/lib/libnickel_lang.so ]; then
                  ln -sf ${cApi}/lib/libnickel_lang.so $out/lib/libnickel_lang.so
                fi
                if [ -e ${cApi}/lib/libnickel_lang.dylib ]; then
                  ln -sf ${cApi}/lib/libnickel_lang.dylib $out/lib/libnickel_lang.dylib
                fi
                ln -sf ${cApi}/include/nickel_lang.h $out/include/nickel_lang.h
              '';
            };

          # In addition to external dependencies, we build the lalrpop file in a
          # separate derivation because it's expensive to build but needs to be
          # rebuilt infrequently.
          cargoArtifacts = craneLib.buildPackage {
            inherit pname version;
            pnameSuffix = "-parser-lalrpop";
            src = craneLib.mkDummySrc {
              inherit src;

              # after stubbing out, reset things back just enough for lalrpop build
              extraDummyScript = ''
                mkdir -p $out/parser/src/
                cp ${./parser/build.rs} $out/parser/build.rs
                cp ${./parser/src/grammar.lalrpop} $out/parser/src/grammar.lalrpop
              '' +
              # package.build gets set to a dummy file. reset it to use local build.rs
              # tomlq -i broken (https://github.com/kislyuk/yq/issues/130 not in nixpkgs yet)
              ''
                ${pkgs.yq}/bin/tomlq -t 'del(.package.build)' $out/parser/Cargo.toml > tmp
                mv tmp $out/parser/Cargo.toml
              '';
            };

            cargoArtifacts = cargoArtifactsDeps;
            cargoExtraArgs = "--package nickel-lang-core";
            # the point of this is to cache lalrpop compilation
            doInstallCargoArtifacts = true;
            # we need the target/ directory to be writable
            installCargoArtifactsMode = "use-zstd";
            CARGO_PROFILE = profile;
          };
        in
        rec {
          inherit cargoArtifacts cargoArtifactsDeps;
          nickel-lang = fixupGitRevision (buildWorkspace {
            extraArgs = {
              inherit env;
              meta.mainProgram = "nickel";
            };
          });
          # A version of the Nickel CLI with the experimental package management
          # feature enabled.
          nickel-lang-pkg = fixupGitRevision (buildWorkspace {
            pnameSuffix = "-pkg";
            extraNickelFeatures = [ "package-experimental" ];
            extraArgs = {
              inherit env;
              meta.mainProgram = "nickel";
            };
          });

          # A version of the Nickel CLI with the experimental Nix interop
          # feature enabled.
          nickel-lang-nix = fixupGitRevision (buildWorkspace {
            pnameSuffix = "-nix";
            extraNickelFeatures = [ "nix-experimental" ];
            extraArgs = {
              inherit env;
              meta.mainProgram = "nickel";
            };
          });

          # Static building isn't really possible on MacOS because the system
          # call ABIs aren't stable. This output shouldn't be used on MacOS.
          nickel-static =
            fixupGitRevision (buildStaticWorkspace {
              pnameSuffix = "-static";
              extraArgs = { meta.mainProgram = "nickel"; };
            });

          # See nickel-static
          nickel-pkg-static =
            fixupGitRevision (buildStaticWorkspace {
              pnameSuffix = "-pkg-static";
              extraNickelFeatures = [ "package-experimental" ];
              extraArgs = { meta.mainProgram = "nickel"; };
            });

          nickel-lang-c = shrinkStaticLib { pnameSuffix = "-c-api"; };

          nickel-lang-c-test = pkgs.stdenv.mkDerivation {
            name = "nickel-lang-c-test";
            inherit version;
            src = ./nickel/capi-tests;

            buildPhase = ''
              mkdir -p $out/bin
              $CC -I "${nickel-lang-c}/include/" run_tests.c "${nickel-lang-c}/lib/libnickel_lang.a" -lm -o $out/bin/run_tests
            '';

            doCheck = true;
            checkPhase = ''
              $out/bin/run_tests
            '';
          };

          benchmarks = craneLib.mkCargoDerivation {
            inherit pname src version cargoArtifacts env;

            pnameSuffix = "-bench";

            # If we aren't running the benchmarks, use the provided profile for maximum cache re-use.
            # (If we're running the benchmarks, let `cargo bench` choose the profile.)
            buildPhaseCargoCommand = ''
              cargo bench -p nickel-lang-core ${pkgs.lib.optionalString noRunBench "--no-run --profile ${profile}"}
            '';

            doInstallCargoArtifacts = false;
          };

          # Check that documentation builds without warnings or errors
          checkRustDoc = craneLib.cargoDoc {
            inherit pname src version cargoArtifacts env;
            inherit (cargoArtifactsDeps) nativeBuildInputs buildInputs;

            RUSTDOCFLAGS = "-D warnings";

            cargoExtraArgs = "${cargoBuildExtraArgs} --workspace --all-features";

            doInstallCargoArtifacts = false;
          };

          rustfmt = craneLib.cargoFmt {
            # Notice that unlike other Crane derivations, we do not pass `cargoArtifacts` to `cargoFmt`, because it does not need access to dependencies to format the code.
            inherit pname src;

            cargoExtraArgs = "--all";

            # `-- --check` is automatically prepended by Crane
            rustFmtExtraArgs = "--color always";
          };

          clippy = craneLib.cargoClippy {
            inherit pname src cargoArtifacts env;
            inherit (cargoArtifactsDeps) nativeBuildInputs buildInputs;

            cargoExtraArgs = cargoBuildExtraArgs;
            cargoClippyExtraArgs = "--all-features --all-targets --workspace -- --deny warnings --allow clippy::new-without-default --allow clippy::match_like_matches_macro";
            CARGO_PROFILE = "dev";
          };
        };

      makeDevShell = { rust }: pkgs.mkShell {
        # Get deps needed to build. Get them from cargoArtifactsDeps so we build
        # the minimal amount possible to get there. It is a waste of time to
        # build the cargoArtifacts, because cargo won't use them anyways.
        inputsFrom = [ (mkCraneArtifacts { inherit rust; profile = "dev"; }).cargoArtifactsDeps ];

        buildInputs = [
          pkgs.cargo-flamegraph
          pkgs.cargo-insta
          pkgs.cargo-nextest
          pkgs.rust-cbindgen
          pkgs.nixpkgs-fmt
          pkgs.nodejs
          pkgs.yarn
          pkgs.yq
          pkgs.nodePackages.markdownlint-cli
          pkgs.python3
        ];

        shellHook = (pre-commit-builder { inherit rust; checkFormat = true; }).shellHook + ''
          echo "=== Nickel development shell ==="
          echo "Info: Git hooks can be installed using \`pre-commit install\`"
        '';

        RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
      };

      # Profile is passed to `wasm-pack`, and is either "dev" (with debug
      # symbols and no optimization), "release" (with optimization and without
      # debug symbols) or "profiling". Right now only dev and release are used:
      #   - release for the production build
      #   - dev for checks where we don't care about optimizations but WASM
      #     optimization takes time
      buildNickelWasm =
        { rust ? mkRust { targets = [ "wasm32-unknown-unknown" ]; }
        , profile ? "release"
        }:
        let
          # Build the various Crane artifacts (dependencies, packages, rustfmt, clippy) for a given Rust toolchain
          craneLib = craneOverrideToolchain rust;

          # suffixes get added via pnameSuffix
          pname = "nickel-lang-wasm";

          # Customize source filtering as Nickel uses non-standard-Rust files like `*.lalrpop`.
          src = filterNickelSrc craneLib.filterCargoSources;

          cargoExtraArgs = "-p nickel-wasm-repl --target wasm32-unknown-unknown --frozen --offline";
          # *  --mode no-install prevents wasm-pack from trying to download and
          #   vendor tools like wasm-bindgen, wasm-opt, etc. but use the one
          #   provided by Nix
          # * --no-default-features disable some default features of Nickel that
          #   aren't useful for the WASM REPL (and possibly incompatible with
          #   WASM build)
          wasmPackExtraArgs = "--${profile} --mode no-install -- --no-default-features --frozen --offline";

          # Build *just* the cargo dependencies, so we can reuse all of that work (e.g. via cachix) when running in CI
          cargoArtifacts = craneLib.buildDepsOnly {
            inherit pname src cargoExtraArgs;
            doCheck = false;
            CARGO_PROFILE = profile;
          };

        in
        craneLib.mkCargoDerivation {
          inherit pname cargoArtifacts src;

          buildPhaseCargoCommand = ''
            WASM_PACK_CACHE=.wasm-pack-cache wasm-pack build wasm-repl ${wasmPackExtraArgs}
          '';

          # nickel-lang.org expects an interface `nickel-repl.wasm`, hence the
          # `ln`
          installPhaseCommand = ''
            mkdir -p $out
            cp -r wasm-repl/pkg $out/nickel-repl
            ln -s $out/nickel-repl/nickel_wasm_repl_bg.wasm $out/nickel-repl/nickel_repl.wasm
          '';

          nativeBuildInputs = [
            rust
            pkgs.wasm-pack
            wasm-bindgen-cli
            pkgs.binaryen
            # Used to include the git revision in the Nickel binary, for `--version`
            pkgs.git
          ] ++ systemSpecificPkgs;

          CARGO_PROFILE = profile;
        };

      buildDocker = nickel: pkgs.dockerTools.buildLayeredImage {
        name = "nickel";
        tag = version;
        contents = [
          nickel
        ];
        config = {
          # Must be an array, not a string (https://github.com/moby/docker-image-spec/blob/f1d00ebd2d6d6805170d5543dbca4b850f35f9af/spec.md#image-json-field-descriptions)
          # See https://github.com/tweag/nickel/issues/2261
          Entrypoint = [ (pkgs.lib.getExe nickel) ];
          # Labels that are recognized by GHCR
          # See https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#labelling-container-images
          Labels = {
            "org.opencontainers.image.source" = "https://github.com/tweag/nickel";
            "org.opencontainers.image.description" = "Nickel: better configuration for less";
            "org.opencontainers.image.licenses" = "MIT";
          };
        };
      };

      # Build the Nickel VSCode extension
      vscodeExtension = pkgs.mkYarnPackage {
        pname = "vscode-nickel";
        src = pkgs.lib.cleanSource ./lsp/vscode-extension;

        buildPhase = ''
          # yarn tries to create a .yarn file in $HOME. There's probably a
          # better way to fix this but setting HOME to TMPDIR works for now.
          export HOME="$TMPDIR"
          cd deps/vscode-nickel
          yarn --offline compile
          yarn --offline vsce package --yarn -o $pname.vsix
        '';

        installPhase = ''
          mkdir $out
          mv $pname.vsix $out
        '';

        distPhase = "true";
      };

      # Copy the markdown user manual to $out.
      userManual = pkgs.stdenv.mkDerivation {
        name = "nickel-user-manual-${version}";
        src = ./doc/manual;
        installPhase = ''
          mkdir -p $out
          cp -r ./ $out
        '';
      };

      # Generate the stdlib documentation from `nickel doc` as `format`.
      stdlibDoc = format:
        let
          extension =
            {
              "markdown" = "md";
            }."${format}" or format;
        in
        pkgs.stdenv.mkDerivation {
          name = "nickel-stdlib-doc-${format}-${version}";
          src = ./core/stdlib;
          installPhase = ''
            mkdir -p $out
            for file in $(ls *.ncl | grep -v 'internals.ncl')
            do
              module=$(basename $file .ncl)
              ${pkgs.lib.getExe self.packages."${system}".default} doc --format "${format}" "$module.ncl" \
                --output "$out/$module.${extension}"
            done
          '';
        };

      stdlibTests = pkgs.runCommandLocal "stdlib-test" { }
        ''
          ${pkgs.lib.getExe self.packages."${system}".default} test ${./core/stdlib/std.ncl} && mkdir $out
        '';

      # This flake used to build nickel-lang-cli and nickel-lang-lsp packages with separate
      # crane invocations. We switched to building the full workspace for better cargo caching,
      # but we also support the old packages for backwards-compatibility.
      compatPackage = { nickel-lang, binary, name }: pkgs.runCommand name
        {
          meta.mainProgram = binary;
        } ''
        mkdir -p $out/bin
        cp -r "${nickel-lang}/bin/${binary}" $out/bin/
      '';
    in
    rec {
      packages = {
        inherit (mkCraneArtifacts { })
          nickel-lang
          nickel-lang-pkg
          nickel-lang-nix
          nickel-lang-c
          benchmarks
          cargoArtifacts;
        default = packages.nickel-lang;

        nickel-lang-cli = compatPackage { inherit (packages) nickel-lang; binary = "nickel"; name = "nickel-lang-cli"; };
        nickel-lang-lsp = compatPackage { inherit (packages) nickel-lang; binary = "nls"; name = "nickel-lang-lsp"; };
        nickelWasm = buildNickelWasm { };
        dockerImage = buildDocker packages.nickel-lang; # TODO: docker image should be a passthru
        inherit vscodeExtension;
        inherit userManual;
        stdlibMarkdown = stdlibDoc "markdown";
        stdlibJson = stdlibDoc "json";
      } // pkgs.lib.optionalAttrs (!pkgs.stdenv.hostPlatform.isDarwin) {
        inherit (mkCraneArtifacts { }) nickel-static nickel-pkg-static;
        # Use the statically linked binary for the docker image if we're not on MacOS.
        dockerImage = buildDocker packages.nickel-static;
      };

      apps = {
        default = {
          type = "app";
          program = pkgs.lib.getExe packages.nickel-lang;
        };
      };

      devShells.default = makeDevShell {
        rust = mkRust {
          rustProfile = "default";
          targets = [ "wasm32-unknown-unknown" ];
          rustExtensions = [
            "rustfmt"
            "clippy"
            "rust-src"
            "rust-analyzer"
          ];
        };
      };

      checks = {
        inherit (mkCraneArtifacts { noRunBench = true; profile = "dev"; })
          benchmarks
          clippy
          checkRustDoc
          nickel-lang
          nickel-lang-c-test
          nickel-lang-pkg
          rustfmt;
        nickelWasm = buildNickelWasm { profile = "dev"; };
        inherit vscodeExtension stdlibTests;
        pre-commit = pre-commit-builder { };
      };
    });
}
